package cn.enited.devtools.apiaggregator.repo.impl;

import cn.enited.devtools.apiaggregator.anno.QueryField;
import cn.enited.devtools.apiaggregator.consts.CommonConst;
import cn.enited.devtools.apiaggregator.consts.ErrorCode;
import cn.enited.devtools.apiaggregator.repo.BaseRepository;
import cn.enited.devtools.apiaggregator.repo.group.AllMethodGroup;
import cn.enited.devtools.apiaggregator.repo.group.FindListGroup;
import cn.enited.devtools.apiaggregator.repo.group.FindOneGroup;
import cn.enited.devtools.apiaggregator.repo.group.FindPageGroup;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.enited.devtools.apiaggregator.entity.BaseEntity;
import cn.enited.devtools.apiaggregator.exception.BizException;
import cn.enited.devtools.apiaggregator.utils.NumUtil;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.bson.Document;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.support.PageableExecutionUtils;

import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
public abstract class AbstractRepository<T extends BaseEntity> implements BaseRepository<T>, InitializingBean {

    private static Map<Class<?>, Map<String, QueryField>> QUERY_FIELD_ANNOTATIONS = new ConcurrentHashMap<>();

    private static final String DEFAULT_SORT_FIELD = "sort";

    @Getter
    private MongoTemplate mongoTemplate;

    @Autowired
    void setMongoTemplate(MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        QUERY_FIELD_ANNOTATIONS.putIfAbsent(getEntityType(), new HashMap<>());
        Map<String, QueryField> queryFieldMap = QUERY_FIELD_ANNOTATIONS.get(getEntityType());
        Map<String, Field> fieldMap = ReflectUtil.getFieldMap(getEntityType());
        fieldMap.forEach((name, field) -> {
            field.setAccessible(true);
            QueryField annotation = field.getAnnotation(QueryField.class);
            if (Objects.nonNull(annotation)) {
                String fieldName = StrUtil.isNotBlank(annotation.name()) ? annotation.name() : name;
                queryFieldMap.putIfAbsent(fieldName, annotation);
            }
        });
    }

    @Override
    public Optional<T> getByBizId(Long bizId) {
        Query query = getBizIdQuery(bizId);
        setIncludeFieldsIfAbsent(query, FindOneGroup.class);
        return Optional.ofNullable(mongoTemplate.findOne(query, getEntityType()));
    }

    @Override
    public List<T> findAll(Query query) {
        setDelFlagCriteriaIfAbsent(query);
        setSortableIfAbsent(query);
        setIncludeFieldsIfAbsent(query, FindListGroup.class);
        return mongoTemplate.find(query, getEntityType());
    }

    @Override
    public Optional<T> findOne(Query query) {
        setDelFlagCriteriaIfAbsent(query);
        setSortableIfAbsent(query);
        setIncludeFieldsIfAbsent(query, FindOneGroup.class);
        return Optional.ofNullable(mongoTemplate.findOne(query, getEntityType()));
    }

    @Override
    public Page<T> findPage(Query query, int pageNumber, int pageSize) {
        setDelFlagCriteriaIfAbsent(query);
        setSortableIfAbsent(query);
        PageRequest pageRequest = buildPageRequest(pageNumber, pageSize);
        query.with(pageRequest);
        setIncludeFieldsIfAbsent(query, FindPageGroup.class);
        long total = mongoTemplate.count(query, getEntityType());
        List<T> content =  mongoTemplate.find(query, getEntityType());
        return PageableExecutionUtils.getPage(content, pageRequest, () -> total);
    }

    /**
     * 设置逻辑删除字段的查询条件，避免忘记
     * @param query
     */
    protected void setDelFlagCriteriaIfAbsent(Query query) {
        Document doc = query.getQueryObject();
        doc.putIfAbsent(CommonConst.ENTITY.DEL_FLAG, Criteria.where(CommonConst.ENTITY.DEL_FLAG).is(false));
    }

    /**
     * 设置默认排序
     * @param query
     */
    protected void setSortableIfAbsent(Query query) {
        if (!query.isSorted()) {
            // 如果实体中有sort字段则按照sort排序，否则按创建时间倒序排序
            if (QUERY_FIELD_ANNOTATIONS.get(getEntityType()).containsKey(DEFAULT_SORT_FIELD)) {
                query.with(Sort.by(Sort.DEFAULT_DIRECTION));
            } else {
                query.with(Sort.by(Sort.Order.desc(CommonConst.ENTITY.CREATE_DATE)));
            }
        }
    }

    /**
     * 设置默认的返回字段
     * @param query
     * @param fieldGroupClasses
     */
    protected void setIncludeFieldsIfAbsent(Query query, Class<?>... fieldGroupClasses) {
        if (MapUtil.isEmpty(query.getFieldsObject())) {
//            query.fields().exclude(CommonConst.ENTITY.ID, CommonConst.ENTITY.DEL_FLAG);
            List<String> fields = getQueryField(fieldGroupClasses);
            if (CollectionUtil.isNotEmpty(fields)) {
                query.fields().include(ArrayUtil.toArray(fields, String.class));
            }
        }
    }

    @Override
    public long count(Query query) {
        return mongoTemplate.count(query, getEntityType());
    }

    @Override
    public boolean exists(Long bizId) {
        return exists(getBizIdQuery(bizId));
    }

    @Override
    public boolean exists(Query query) {
        return mongoTemplate.exists(query, getEntityType());
    }

    @Override
    public Page<T> findPage(Query query, int pageNumber) {
        return findPage(query, pageNumber, CommonConst.DEFAULT_PAGE_SIZE);
    }

    @Override
    public void save(T entity) throws BizException {
        log.error(this.getClass().getName() + "未实现save方法");
        throw new BizException(ErrorCode.NOT_IMPLEMENT);
    }

    @Override
    public void update(T entity) throws BizException {
        log.error(this.getClass().getName() + "未实现update方法");
        throw new BizException(ErrorCode.NOT_IMPLEMENT);
    }

    @Override
    public void delete(Long bizId) throws BizException {
        Update update = Update.update(CommonConst.ENTITY.DEL_FLAG, true);
        update.set(CommonConst.ENTITY.UPDATE_DATE, DateUtil.date());
        mongoTemplate.updateFirst(getBizIdQuery(bizId), update, getEntityType());
    }

    /**
     * 生成bizId查询条件
     * @param bizId
     * @return
     */
    Query getBizIdQuery(Long bizId) {
        return Query.query(Criteria.where(CommonConst.ENTITY.BIZ_ID).is(bizId).and(CommonConst.ENTITY.DEL_FLAG).is(false));
    }

    /**
     * entity属性转为map，主要用于update
     * @param entity
     * @param ignoreProperties
     * @return
     */
    Document entityToUpdateMap(T entity, String... ignoreProperties) {
        Map<String, Object> map = BeanUtil.beanToMap(entity, false, false);
        Document document = new Document();
        if (MapUtil.isNotEmpty(map)) {
            if (ArrayUtil.isNotEmpty(ignoreProperties)) {
                MapUtil.removeAny(map, ignoreProperties);
            }
            document.putAll(map);
        }
        Document updateDoc = new Document();
        updateDoc.put("$set", document);
        return updateDoc;
    }

    /**
     * 构建分页请求
     * @param pageNumber
     * @param pageSize
     * @return
     */
    private PageRequest buildPageRequest(int pageNumber, int pageSize) {
        int page = NumUtil.isValidPageNumber(pageNumber) ? pageNumber - 1 : CommonConst.ZERO.INT;
        int ps = NumUtil.isValidSize(pageSize) ? pageSize : CommonConst.DEFAULT_PAGE_SIZE;
        return PageRequest.of(page, ps);
    }

    /**
     * 从Entity中获取include的字段
     * @param groupClasses
     * @return
     */
    private List<String> getQueryField(Class<?>... groupClasses) {
        List<String> includeFields = new ArrayList<>();
        if (ArrayUtil.isEmpty(groupClasses)) {
            return includeFields;
        }

        QUERY_FIELD_ANNOTATIONS.get(getEntityType()).forEach((fieldName, annotation) -> {
            if (ArrayUtil.contains(annotation.groups(), AllMethodGroup.class)
                    || ArrayUtil.containsAny(annotation.groups(), groupClasses)) {
                includeFields.add(fieldName);
            }
        });
        return includeFields;
    }
}
